package com.xiaotu.spider.downloader;

import java.io.IOException;
import java.net.URI;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.ProtocolException;
import org.apache.http.annotation.ThreadSafe;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthState;
import org.apache.http.client.RedirectException;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpExecutionAware;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.impl.execchain.ClientExecChain;
import org.apache.http.util.Args;
import org.apache.http.util.EntityUtils;

/**
 * @类名 MyRedirectExec
 * @日期 2017年3月21日
 * @作者 高海军
 * @功能
 */
@ThreadSafe
public class MyRedirectExec implements ClientExecChain
{
	private final Log log = LogFactory.getLog(getClass());
	
	private final ClientExecChain requestExecutor;
	private final RedirectStrategy redirectStrategy;
	private final HttpRoutePlanner routePlanner;
	
	public MyRedirectExec(final ClientExecChain requestExecutor,
			final HttpRoutePlanner routePlanner,
			final RedirectStrategy redirectStrategy)
	{
		super();
		Args.notNull(requestExecutor, "HTTP client request executor");
		Args.notNull(routePlanner, "HTTP route planner");
		Args.notNull(redirectStrategy, "HTTP redirect strategy");
		this.requestExecutor = requestExecutor;
		this.routePlanner = routePlanner;
		this.redirectStrategy = redirectStrategy;
	}
	
	@Override
	public CloseableHttpResponse execute(HttpRoute route,
			HttpRequestWrapper request, HttpClientContext context,
			HttpExecutionAware execAware) throws IOException, HttpException
	{
		Args.notNull(route, "HTTP route");
		Args.notNull(request, "HTTP request");
		Args.notNull(context, "HTTP context");
		
		final List<URI> redirectLocations = context.getRedirectLocations();
		if (redirectLocations != null)
		{
			redirectLocations.clear();
		}
		
		final RequestConfig config = context.getRequestConfig();
		final int maxRedirects = config.getMaxRedirects() > 0
				? config.getMaxRedirects() : 50;
		HttpRoute currentRoute = route;
		HttpRequestWrapper currentRequest = request;
		for (int redirectCount = 0;;)
		{
			final CloseableHttpResponse response = requestExecutor
					.execute(currentRoute, currentRequest, context, execAware);
			try
			{
				if (config.isRedirectsEnabled() && this.redirectStrategy
						.isRedirected(currentRequest, response, context))
				{
					
					if (redirectCount >= maxRedirects)
					{
						throw new RedirectException("Maximum redirects ("
								+ maxRedirects + ") exceeded");
					}
					redirectCount++;
					
					final HttpRequest redirect = this.redirectStrategy
							.getRedirect(currentRequest, response, context);
					if (!redirect.headerIterator().hasNext())
					{
						final HttpRequest original = request.getOriginal();
						Header[] headers = original.getAllHeaders();
						for (Header header : headers)
							if (!SpiderHttpClientDownloader.HEADER_HOST
									.equals(header.getName()))
								redirect.setHeader(header);
						redirect.setHeader(
								SpiderHttpClientDownloader.HEADER_REFERER,
								redirect.getRequestLine().getUri());
					}
					currentRequest = HttpRequestWrapper.wrap(redirect);
					
					if (currentRequest instanceof HttpEntityEnclosingRequest)
					{
						RequestEntityProxy.enhance(
								(HttpEntityEnclosingRequest) currentRequest);
					}
					
					final URI uri = currentRequest.getURI();
					final HttpHost newTarget = URIUtils.extractHost(uri);
					if (newTarget == null)
					{
						throw new ProtocolException(
								"Redirect URI does not specify a valid host name: "
										+ uri);
					}
					
					// Reset virtual host and auth states if redirecting to
					// another host
					if (!currentRoute.getTargetHost().equals(newTarget))
					{
						final AuthState targetAuthState = context
								.getTargetAuthState();
						if (targetAuthState != null)
						{
							this.log.debug("Resetting target auth state");
							targetAuthState.reset();
						}
						final AuthState proxyAuthState = context
								.getProxyAuthState();
						if (proxyAuthState != null)
						{
							final AuthScheme authScheme = proxyAuthState
									.getAuthScheme();
							if (authScheme != null
									&& authScheme.isConnectionBased())
							{
								this.log.debug("Resetting proxy auth state");
								proxyAuthState.reset();
							}
						}
					}
					
					currentRoute = this.routePlanner.determineRoute(newTarget,
							currentRequest, context);
					if (this.log.isDebugEnabled())
					{
						this.log.debug("Redirecting to '" + uri + "' via "
								+ currentRoute);
					}
					EntityUtils.consume(response.getEntity());
					response.close();
				}
				else
				{
					return response;
				}
			}
			catch (final RuntimeException ex)
			{
				response.close();
				throw ex;
			}
			catch (final IOException ex)
			{
				response.close();
				throw ex;
			}
			catch (final HttpException ex)
			{
				// Protocol exception related to a direct.
				// The underlying connection may still be salvaged.
				try
				{
					EntityUtils.consume(response.getEntity());
				}
				catch (final IOException ioex)
				{
					this.log.debug("I/O error while releasing connection",
							ioex);
				}
				finally
				{
					response.close();
				}
				throw ex;
			}
		}
	}
}
